import time

import coin.exchange.base.kr_rest.private_client_base as privcb
import coin.exchange.bitfinex_v2.kr_rest.native_private_client as nprivc
from coin.exchange.bitfinex_v2.kr_rest.constants import currency_list

import coin.proto.coin_order_enums_pb2 as coin_order
from coin.exchange.bitfinex_v2.kr_rest.product import (BitfinexCurrency, BitfinexProduct)
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       AccountPosition,
                                       CurrencyBalance,
                                       ProductFillElement,
                                       ProductFillBundle,
                                       ProductOrderElement,
                                       ProductOrderBundle,
                                       ProductPosition)
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.bitfinex_v2.kr_rest.product import BitfinexProduct


class BitfinexPrivateParser(object):
  @staticmethod
  def parse_balance(update_msg_list: list, wallet_type='exchange'):
    currency_balance_list = []
    for update_msg in update_msg_list:
      # There are three types of balance in Bitfinex:
      # "exchange", "margin", "fund"
      if update_msg['wallet_type'] != wallet_type:
        continue
      native_currency = update_msg['currency']
      currency = BitfinexCurrency.FromStrNativeCurrencyNothrow(native_currency)
      if currency is None:
        continue

      if update_msg['balance_available'] is None:
        available = float('nan')
      else:
        available = update_msg['balance_available']
      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         total=float(update_msg['balance']),
                                         available=available)
      currency_balance_list.append(currency_balance)

    return AccountBalance(exchange='Bitfinex', each_balance=currency_balance_list)

  @staticmethod
  def parse_fill(update_msg_list: list, product):
    """Sample fill msg given by Bitfinex V2.
    {
        "id": 193541945,
        "pair": "tETHUSD",
        "mts_create": 1518352550000,
        "order_id": 8192377272,
        "exec_amount": 0.35863,
        "exec_price": 835,
        "order_type": "EXCHANGE LIMIT",
        "order_price": 835,
        "maker": null,
        "fee": -0.00071726,
        "fee_currency": "ETH"
    }
    """
    currency_fill_list = []
    for update_msg in update_msg_list:
      if update_msg['pair'] != product.native_symbol:
        continue
      currency_fill = ProductFillElement(symbol=product.symbol,
                                         price=float(update_msg['exec_price']),
                                         qty=float(update_msg['exec_amount']),
                                         fee=float(update_msg['fee']),
                                         order_id=str(update_msg['order_id']),
                                         fill_timestamp=int(update_msg['mts_create']) * 10**6)
      currency_fill_list.append(currency_fill)

    return ProductFillBundle(exchange='Bitfinex',
                             symbol=product.symbol,
                             each_fill=currency_fill_list)

  @staticmethod
  def parse_position(update_msg_list: list):
    product_position_list = []
    for position_info in update_msg_list:
      native_symbol = position_info['symbol']
      product = BitfinexProduct.FromStrNativeProductNothrow(native_symbol)
      if product is None:
        continue

      net_position = float(position_info['amount'])
      total_pnl = float(position_info['pl'])
      liquidation_price = float(position_info['price_liq'])
      product_position = ProductPosition(symbol=product.symbol,
                                         native_symbol=native_symbol,
                                         net_position=net_position,
                                         total_pnl=total_pnl,
                                         liquidation_price=liquidation_price)
      product_position_list.append(product_position)
    return AccountPosition(exchange='Bitfinex', each_position=product_position_list)

  @staticmethod
  def parse_orders(update_msg_list: list, product):
    product_order_list = []
    for update_elem in update_msg_list:
      if update_elem['symbol'] != product.native_symbol:
        continue
      qty = update_elem['amount_orig']
      if qty > 0:
        side = coin_order.BUY_ORDER
      elif qty < 0:
        side = coin_order.SELL_ORDER
      else:
        raise ValueError('qty cannot be 0')

      order = ProductOrderElement(
          symbol=product.symbol,
          price=update_elem['price'],
          qty=abs(update_elem['amount_orig']),
          side=side,
      )
      product_order_list.append(order)
    return ProductOrderBundle(
        exchange="Bitfinex",
        symbol=product.symbol,
        native_symbol=product.native_symbol,
        each_order=product_order_list,
    )


class BitfinexPrivateClient(privcb.PrivateClientBase):
  ProductType = BitfinexProduct

  def __init__(self, key_file):
    privcb.PrivateClientBase.__init__(self)
    self.nprivc = nprivc.BitfinexNativePrivateClient(key_file=key_file)

  def _generate_account_balance_update(self):
    reqtime = time.time()
    update = self.nprivc.query_wallets()
    restime = time.time()
    keys = ['wallet_type', 'currency', 'balance', 'unsettled_interest', 'balance_available']
    update_list = []
    for each in update:
      update_list.append(dict(zip(keys, each)))
    update = RestUpdate(reqtime, restime, update_list)
    return update

  def query_account_balance_impl(self):
    update = self._generate_account_balance_update()
    update.msg = BitfinexPrivateParser.parse_balance(update.msg)
    return update

  def query_fills_impl(self, product):
    reqtime = time.time()
    update = self.nprivc.query_trades(product)
    restime = time.time()
    keys = [
        "id",
        "pair",
        "mts_create",
        "order_id",
        "exec_amount",
        "exec_price",
        "order_type",
        "order_price",
        "maker",
        "fee",
        "fee_currency"
    ]
    update_list = []
    for each in update:
      update_list.append(dict(zip(keys, each)))

    update = RestUpdate(reqtime, restime, update_list)
    update.msg = BitfinexPrivateParser.parse_fill(update.msg, product)
    return update

  def query_list_orders_impl(self, product):
    reqtime = time.time()
    update = self.nprivc.query_orders()
    restime = time.time()
    keys = [
        "id",
        "gid",
        "cid",
        "symbol",
        "mts_create",
        "mts_update",
        "amount",
        "amount_orig",
        "type",
        "type_prev",
        "_placeholder1",
        "_placeholder2",
        "flags",
        "status",
        "_placeholder3",
        "_placeholder4",
        "price",
        "price_avg",
        "price_trailing",
        "price_aux_limit",
        "_placeholder5",
        "_placeholder6",
        "_placeholder7",
        "hidden",
        "placed_id"
    ]

    update_list = []
    for each in update:
      update_list.append(dict(zip(keys, each)))
    update = RestUpdate(reqtime, restime, update_list)
    update.msg = BitfinexPrivateParser.parse_orders(update.msg, product)
    return update

  def query_submit_impl(self, product, side, price, qty):
    raise NotImplementedError()

  def query_cancel_product_impl(self, product):
    raise NotImplementedError()


class BitfinexMarginPrivateClient(BitfinexPrivateClient):
  ProductType = BitfinexProduct

  def query_account_balance_impl(self):
    update = self._generate_account_balance_update()
    update.msg = BitfinexPrivateParser.parse_balance(update.msg, wallet_type='margin')
    return update

  def query_account_position_impl(self):
    reqtime = time.time()
    update = self.nprivc.query_positions()
    restime = time.time()
    keys = [
        'symbol',
        'status',
        'amount',
        'base_price',
        'margin_funding',
        'margin_funding_type',
        'pl',
        'pl_perc',
        'price_liq',
        'leverage'
    ]
    update_list = []
    for each in update:
      update_list.append(dict(zip(keys, each)))
    update = RestUpdate(reqtime, restime, update_list)
    update.msg = BitfinexPrivateParser.parse_position(update_list)
    return update
